-
Notifications
You must be signed in to change notification settings - Fork 19
Memory Optimizations #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory Optimizations #644
Conversation
📝 WalkthroughWalkthroughThis PR shifts model mutations and retrieval from Prolog-only paths to a metadata-driven SurrealDB flow with Prolog-first fallback, introduces Prolog execution modes and per-context/pooled engines, adds shared Whisper model caching, SurrealQL escaping and SDNA helpers, adds DB predicate filtering, and updates/skips multiple Prolog-dependent tests. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant PerspectiveProxy
participant Ad4mModel
participant PrologService
participant SurrealDB
Client->>PerspectiveProxy: request data
PerspectiveProxy->>Ad4mModel: getData(baseExpression, metadata)
alt Prolog-first
Ad4mModel->>PrologService: run query
PrologService-->>Ad4mModel: results / empty / error
alt Prolog succeeded
Ad4mModel->>Ad4mModel: apply metadata transforms & resolve language
Ad4mModel-->>PerspectiveProxy: mapped data
else
Ad4mModel->>SurrealDB: metadata-driven SurrealQL query (escaped)
SurrealDB-->>Ad4mModel: links/rows
Ad4mModel->>Ad4mModel: map values (latest-wins / collection order)
Ad4mModel-->>PerspectiveProxy: mapped data
end
else SdnaOnly / Prolog disabled
Ad4mModel->>SurrealDB: direct metadata-driven SurrealQL query
SurrealDB-->>Ad4mModel: rows
Ad4mModel-->>PerspectiveProxy: mapped data
end
PerspectiveProxy-->>Client: response
sequenceDiagram
actor Client
participant PerspectiveInstance
participant PrologService
participant SimpleEngine
participant PrologEnginePool
Client->>PerspectiveInstance: execute query
alt PrologMode == Simple or SdnaOnly
PerspectiveInstance->>PrologService: run_query_simple(query)
PrologService->>SimpleEngine: ensure engine updated if dirty
SimpleEngine->>SimpleEngine: execute query
SimpleEngine-->>PrologService: results
else PrologMode == Pooled
PerspectiveInstance->>PrologService: _run_query(query, context)
PrologService->>PrologEnginePool: ensure/get pool for context
PrologEnginePool->>PrologEnginePool: execute query
PrologEnginePool-->>PrologService: results
else PrologMode == Disabled
PrologService-->>PerspectiveInstance: return no-op/empty
end
PerspectiveInstance-->>Client: respond
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
rust-executor/src/perspectives/perspective_instance.rs (1)
3984-4017: Privacy/logging: avoid logging query contents atinfo(may include sensitive data).The periodic log prints subscription IDs plus a query preview at
info. Queries can embed user data (or identifiers), andinfocan end up in production logs.Suggested change: log only counts at
info, and move query previews todebug(or guard behind a feature flag).
🤖 Fix all issues with AI agents
In `@core/src/model/Ad4mModel.ts`:
- Around line 814-822: The literal parsing branch incorrectly expects
Literal.fromUrl(value).get() to return an object with a data property; instead
assign the returned parsed value directly to value. Locate the block using
Literal.fromUrl(value).get() (inside the string 'literal://' branch) and replace
the conditional that checks parsed.data !== undefined with a direct assignment
value = parsed (while retaining the try/catch to fall back to the original value
on error).
In `@rust-executor/src/perspectives/perspective_instance.rs`:
- Around line 1382-1420: get_sdna_links_local() claims to perform two targeted
queries but relies on get_links_local(), whose predicate branch currently calls
db.get_all_links(&uuid) and filters in-memory, negating the targeted behavior;
either add an indexed DB accessor such as Ad4mDb::get_links_by_predicate(&uuid,
predicate) and update get_links_local() (or call that DB method directly from
get_sdna_links_local()) so predicate queries hit the index, or if you cannot add
a DB API now, change the get_sdna_links_local() doc comment to stop claiming
“two targeted queries” and clearly state it may scan all links; update
references to get_links_local, get_sdna_links_local, and consider adding
Ad4mDb::get_links_by_predicate to align callers and tests.
- Around line 2518-2532: The Simple-mode branch in spawn_prolog_facts_update is
causing duplicate pubsub events because it calls
self_clone.pubsub_publish_diff(diff).await before returning; remove that publish
call from the PrologMode::Simple early-return path (keep marking
trigger_prolog_subscription_check and sending completion_sender if present) so
publishing is performed only by the caller and not duplicated inside the
tokio::spawn block.
♻️ Duplicate comments (1)
core/src/perspectives/PerspectiveProxy.ts (1)
1414-1431: Add literal URL decoding for property values.When
resolveLanguageis not set but the retrieved value is aliteral://URL, the method returns the raw URL string instead of the decoded value. This was noted in a previous review.Proposed fix
const value = result[0].value; + // Decode literal URLs + if (typeof value === "string" && value.startsWith("literal://")) { + try { + return Literal.fromUrl(value).get(); + } catch (e) { + // Fall through to existing handling + } + } + // Handle expression resolution if needed if (propMeta.resolveLanguage && value) {
🧹 Nitpick comments (5)
core/src/perspectives/PerspectiveProxy.ts (1)
1524-1535: Minor: Unused variablefirstSet.The variable
firstSeton line 1529 is declared but not used. The filtering logic correctly usesvalidExpressionSets.every()instead.Proposed cleanup
// Find intersection: expressions that passed ALL required triple checks if (validExpressionSets.length === 0) { return expressions; } - const firstSet = validExpressionSets[0]; const validExpressions = expressions.filter(expr => { return validExpressionSets.every(set => set.has(expr)); });core/src/model/Ad4mModel.ts (1)
1366-1384: Consider usingescapeSurrealStringinternally to reduce duplication.
formatSurrealValueduplicates the escaping logic fromescapeSurrealString. Consider refactoring to use the imported utility internally for consistency and reduced maintenance burden.Proposed refactor
private static formatSurrealValue(value: any): string { if (typeof value === 'string') { - // Escape backslashes first, then single quotes and other special characters - const escaped = value - .replace(/\\/g, '\\\\') // Backslash -> \\ - .replace(/'/g, "\\'") // Single quote -> \' - .replace(/"/g, '\\"') // Double quote -> \" - .replace(/\n/g, '\\n') // Newline -> \n - .replace(/\r/g, '\\r') // Carriage return -> \r - .replace(/\t/g, '\\t'); // Tab -> \t - return `'${escaped}'`; + return `'${escapeSurrealString(value)}'`; } else if (typeof value === 'number' || typeof value === 'boolean') {rust-executor/src/prolog_service/mod.rs (1)
225-235: Consider usingOptionortakepattern instead of placeholder engines.The
std::mem::replacewithPrologEngine::new()placeholders works but creates unspawned engine instances that are immediately discarded. This is minor overhead.💡 Alternative using Option pattern
Wrapping the engines in
Option<PrologEngine>inSimpleEnginewould allow usingtake()without creating placeholders:// In SimpleEngine struct: query_engine: Option<PrologEngine>, subscription_engine: Option<PrologEngine>, // Then use: let query_engine_to_update = simple_engine.query_engine.take().unwrap(); let subscription_engine_to_update = simple_engine.subscription_engine.take().unwrap(); // And restore: simple_engine.query_engine = Some(query_engine_to_update); simple_engine.subscription_engine = Some(subscription_engine_to_update);rust-executor/src/perspectives/perspective_instance.rs (2)
795-805: Reduce duplication: extract a helper for “mark query engine dirty in Simple mode”.The same
if PROLOG_MODE == PrologMode::Simple { ... mark_dirty(...) }block repeats across multiple mutation paths; it also re-lockspersistedeach time.Possible refactor sketch
+ async fn mark_simple_query_engine_dirty(&self) { + if PROLOG_MODE != PrologMode::Simple { + return; + } + let perspective_uuid = self.persisted.lock().await.uuid.clone(); + get_prolog_service().await.mark_dirty(&perspective_uuid).await; + }Then replace the repeated blocks with:
- if PROLOG_MODE == PrologMode::Simple { - let perspective_uuid = self.persisted.lock().await.uuid.clone(); - get_prolog_service().await.mark_dirty(&perspective_uuid).await; - } + self.mark_simple_query_engine_dirty().await;Also applies to: 911-921, 1045-1055, 1233-1244, 1348-1359, 4256-4267
3827-3886: Good memory win (avoid cloninglast_result), butjoin_allcan still stampede.Avoiding
last_resultclones is a solid improvement, butfuture::join_all(query_futures).awaitwill fire all subscription re-queries concurrently; with many subscriptions, that can spike CPU/memory and hammer Prolog/SurrealDB.Consider bounding concurrency (e.g.,
stream::iter(...).buffer_unordered(N)or a semaphore) to cap simultaneous queries.Also applies to: 3905-3950
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
core/src/model/Ad4mModel.tscore/src/perspectives/PerspectiveProxy.tscore/src/utils.tsrust-executor/src/perspectives/perspective_instance.rsrust-executor/src/perspectives/sdna.rsrust-executor/src/prolog_service/mod.rstests/js/tests/prolog-and-literals.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/js/tests/prolog-and-literals.test.ts
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/perspectives/PerspectiveProxy.ts:1465-1491
Timestamp: 2026-01-15T14:55:56.365Z
Learning: The codebase is moving toward making Prolog completely optional. Do not suggest adding Prolog fallbacks or dependencies in code paths that are being migrated to SurrealDB-based implementations (SdnaOnly mode).
📚 Learning: 2026-01-15T14:55:56.365Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/perspectives/PerspectiveProxy.ts:1465-1491
Timestamp: 2026-01-15T14:55:56.365Z
Learning: The codebase is moving toward making Prolog completely optional. Do not suggest adding Prolog fallbacks or dependencies in code paths that are being migrated to SurrealDB-based implementations (SdnaOnly mode).
Applied to files:
rust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rs
📚 Learning: 2025-04-11T14:17:11.180Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 593
File: rust-executor/src/prolog_service/engine_pool.rs:65-89
Timestamp: 2025-04-11T14:17:11.180Z
Learning: The `run_query_all()` method in PrologEnginePool intentionally uses a write lock (not a read lock) because it can run assert/1 queries that modify facts. This ensures all engines remain synchronized and prevents other operations from executing concurrently during fact updates.
Applied to files:
rust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rs
📚 Learning: 2025-07-17T16:34:09.725Z
Learnt from: jhweir
Repo: coasys/ad4m PR: 617
File: ad4m-hooks/vue/src/useModel.ts:45-56
Timestamp: 2025-07-17T16:34:09.725Z
Learning: In ad4m-hooks/vue/src/useModel.ts, the baseExpression property is defined as a getter on the Ad4mModel prototype, not as an own property on individual instances. The current implementation correctly uses Object.defineProperty to create enumerable own properties that shadow the prototype getter, which is the appropriate approach for making prototype accessors enumerable.
Applied to files:
core/src/model/Ad4mModel.ts
📚 Learning: 2025-09-03T15:16:29.623Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 619
File: rust-executor/src/perspectives/perspective_instance.rs:639-641
Timestamp: 2025-09-03T15:16:29.623Z
Learning: In the AD4M codebase, database operations like `add_many_links` during critical synchronization processes (such as `diff_from_link_language`) should use `.expect()` rather than error logging, as failures indicate severe system issues where fail-fast behavior is preferred over continuing with potentially corrupted state.
Applied to files:
rust-executor/src/prolog_service/mod.rs
📚 Learning: 2026-01-15T14:55:56.365Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/perspectives/PerspectiveProxy.ts:1465-1491
Timestamp: 2026-01-15T14:55:56.365Z
Learning: In code paths that are migrated to SurrealDB-based implementations (SdnaOnly mode), do not introduce Prolog fallbacks or add Prolog dependencies. Apply this guidance to TypeScript files under core/src/perspectives (and related SdnaOnly areas) to ensure Prolog remains optional only where appropriate and is not wired into SurrealDB-driven flows.
Applied to files:
core/src/perspectives/PerspectiveProxy.ts
🧬 Code graph analysis (2)
rust-executor/src/perspectives/perspective_instance.rs (4)
rust-executor/src/prolog_service/mod.rs (2)
get_prolog_service(802-805)new(76-81)rust-executor/src/prolog_service/engine_pool.rs (1)
new(118-138)rust-executor/src/prolog_service/pool_trait.rs (1)
new(74-82)rust-executor/src/prolog_service/filtered_pool.rs (1)
new(311-322)
core/src/perspectives/PerspectiveProxy.ts (2)
core/src/utils.ts (1)
escapeSurrealString(49-57)core/src/Literal.ts (1)
Literal(9-84)
🪛 ast-grep (0.40.5)
core/src/perspectives/PerspectiveProxy.ts
[warning] 1270-1270: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(property_getter\\([^,]+,\\s*[^,]+,\\s*"${escapedPropName}"[^)]*\\)\\s*:-\\s*triple\\([^,]+,\\s*"([^"]+)")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 1313-1313: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(collection_getter\\([^,]+,\\s*[^,]+,\\s*"${escapedCollName}"[^)]*\\)\\s*:-[^.]+\\.)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (23)
core/src/utils.ts (1)
49-57: LGTM! Well-implemented SurrealQL escaping utility.The function correctly handles the most common special characters that could break queries or enable injection attacks. The order of replacements (backslash first) is correct to prevent double-escaping issues.
Minor consideration: For maximum robustness, you might also consider escaping null bytes (
\0) which can cause issues in some database engines, though this is rarely encountered in practice.core/src/perspectives/PerspectiveProxy.ts (5)
416-427: LGTM! Standard regex escaping implementation.This correctly escapes all regex metacharacters to prevent ReDoS attacks and regex injection when building dynamic patterns for SDNA parsing.
1098-1153: LGTM! SQL injection concerns addressed with proper escaping.The implementation now correctly uses
escapeSurrealStringfor all interpolated values in SurrealQL queries (lines 1116, 1126-1131). The count value handling properly accounts for different response formats from SurrealDB.
1175-1244: LGTM! Regex injection concerns addressed with escapeRegExp.The implementation now properly escapes
propNameandcollNamebefore using them in dynamic RegExp patterns (lines 1270, 1313). The static analysis warnings are effectively false positives since the escaped strings cannot contain regex metacharacters.Note: The method makes multiple
infer()calls per property/collection for metadata extraction. For models with many properties, consider caching the metadata after first extraction or batching the Prolog queries if this becomes a performance bottleneck.
1433-1476: LGTM! N+1 query issue addressed with batch checking.The implementation now uses
batchCheckSubjectInstances(line 1468) to efficiently batch-check instance membership instead of individualisSubjectInstancecalls per element. The fallback tofilterInstancesSequentialwhen metadata is unavailable is a reasonable safety net.
1561-1632: LGTM! SurrealDB-based instance retrieval.Both
getAllSubjectInstancesandgetAllSubjectProxiesnow correctly use SurrealDB queries via metadata when available. The intentional omission of a Prolog fallback aligns with the goal of making Prolog completely optional. Based on learnings, this is the intended direction.The
getAllSubjectProxiesmethod now correctly returns Subject proxy instances (addressing a past review concern about returning binding objects).core/src/model/Ad4mModel.ts (5)
618-634: LGTM! Clean metadata accessor helpers.These methods provide a clean interface to access decorator-stored metadata, supporting the Phase 1 migration away from Prolog queries.
636-691: LGTM! Metadata-driven action generation.The action generators correctly replace Prolog queries with metadata-derived actions. The explicit error for custom setters (line 644-647) is a reasonable Phase 1 limitation with a clear error message guiding users.
1991-2083: LGTM! Clean migration to metadata-driven mutations.The property and collection setters now use decorator metadata instead of Prolog queries. The graceful handling of missing metadata with
console.warn(instead of throwing) provides good developer experience during migration. The parallel execution pattern withPromise.allfor collection operations is appropriate.
2183-2201: LGTM! Improved array/collection handling in innerUpdate.The explicit handling of arrays as collections (lines 2183-2188) and the check to skip collection-annotated keys during property updates (lines 2192-2196) correctly prevents the dual-treatment issue. Empty arrays being skipped is reasonable behavior.
1494-1537: LGTM! Robust target value handling.The guard on line 1494 correctly prevents processing of empty, null, or undefined targets. The warning logs for failed expression resolution (lines 1511-1513) are appropriate for debugging. The literal URL fallback (lines 1515-1527) handles edge cases gracefully.
rust-executor/src/perspectives/sdna.rs (1)
113-117: LGTM!The helper function is well-documented and correctly combines the two SDNA-related checks. Using
as_deref()for the Option comparison is idiomatic.rust-executor/src/prolog_service/mod.rs (10)
28-51: LGTM - acknowledged as intentional per prior discussion.The
PrologModeenum and hardcodedSdnaOnlydefault align with the memory optimization goals. The enum design is clean and well-documented.
53-73: LGTM!The
SimpleEnginestruct design with separate query and subscription engines is sound. UsingOption<Vec<...>>forcurrent_sdna_linkscorrectly distinguishes uninitialized state from empty SDNA links.
83-101: LGTM!Mode-gated dirty marking with appropriate lock scope. The method correctly no-ops for Disabled/Pooled modes.
284-326: LGTM!The method correctly gates on Prolog mode, ensures the engine is updated, normalizes the query, and executes through the query engine. Error handling is appropriate.
328-370: LGTM!Properly uses the separate
subscription_engineto avoid interference with regular queries. Structure mirrorsrun_query_simpleappropriately.
372-423: LGTM!Mode gating correctly restricts pool creation to Pooled mode. The optimistic locking with double-check pattern is the right approach for avoiding race conditions.
455-614: LGTM!Consistent mode-gating across all pooled-mode query methods. The error handling for misuse (calling pooled methods in Simple/SdnaOnly mode) with warning logs is helpful for debugging.
644-712: LGTM!Graceful no-op behavior in non-pooled modes with
tracelevel logging (appropriate for non-error conditions). Performance timing instrumentation is useful for debugging.
716-785: LGTM!Mode gating correctly routes Simple/SdnaOnly modes to lazy updates. The trace-level logging for expected no-op behavior is appropriate.
814-920: LGTM!The test is correctly marked as ignored since it exercises Pooled mode functionality which is incompatible with the current
SdnaOnlydefault. The test implementation is valid for when Pooled mode is enabled.rust-executor/src/perspectives/perspective_instance.rs (1)
54-56: Verify client compatibility with the new 60s subscription timeout.Dropping
QUERY_SUBSCRIPTION_TIMEOUTto 60s is a behavioral change; if clients don’t send keepalives reliably (or mobile backgrounding delays them), subscriptions may churn.Please confirm expected keepalive cadence on the client side (and in any proxies) is comfortably < 60s under real-world conditions.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/js/tests/prolog-and-literals.test.ts (1)
421-432: Add a tracking note for this skipped test.
Skipping reduces coverage forInstanceQuery(condition: ..). Please add a TODO/issue link or rationale so it’s clear when to re-enable.
🤖 Fix all issues with AI agents
In `@core/src/model/Ad4mModel.ts`:
- Around line 769-772: The code currently selects the first matching link with
matching[0], which picks the oldest link because links are ordered ASC; change
this to pick the latest link by using the last element (e.g.,
matching[matching.length - 1]) or reverse/order DESC before selecting. Update
the selection in the block that filters links by propMeta.predicate (variables:
links, matching, propMeta.predicate, link) so the "latest wins" semantics are
enforced.
- Around line 2163-2176: innerUpdate currently skips empty arrays so calling
setCollectionSetter(key, value, batchId) only for non-empty arrays prevents
clearing collections by assigning []; change innerUpdate so Array.isArray(value)
always calls setCollectionSetter (remove the value.length check) and ensure
setCollectionSetter handles an empty array by removing/clearing existing
collection links for the given key (so setting [] clears the collection). Update
references in innerUpdate, setCollectionSetter, and any logic that relies on
getCollectionMetadata to treat arrays as collections consistently.
- Around line 641-660: In generatePropertySetterAction, add a guard that checks
metadata.writable (or metadata.readOnly flag if used) and reject setter
generation for read-only properties: if metadata.writable === false then throw a
clear Error like `Property "${key}" is read-only and cannot be written` (place
this check before returning the action and before any custom setter handling so
`@ReadOnly` fields are protected); keep the existing checks for metadata.setter
and metadata.through and preserve the existing action shape when writable is
allowed.
- Around line 1993-2013: The current truthy checks around setter/adder/remover
logic (using if (value)) in Ad4mModel.ts drop valid falsy values like 0, false,
and ""; update the conditional to a nullish check (value !== null && value !==
undefined or value != null) wherever you call generateCollectionAction and then
call this.#perspective.executeAction (referenced symbols: getCollectionMetadata,
generateCollectionAction, this.#perspective.executeAction, this.#baseExpression)
so that legitimate falsy values are passed through; apply the same change to the
other occurrences noted (lines around 2018-2037 and 2042-2061).
- Around line 752-761: The SurrealDB query in getData() (linksQuery) is missing
a perspective constraint, allowing links from other perspectives; update the
WHERE clause in the linksQuery (constructed with safeBaseExpression via
ctor.formatSurrealValue) to also filter by the current perspective
(this.#perspective) — use the same approach you use elsewhere (either bind a
$perspective parameter or escape this.#perspective with formatSurrealValue) so
the query only returns rows where perspective equals the current perspective
before calling this.#perspective.querySurrealDB.
In `@rust-executor/src/db.rs`:
- Around line 1176-1211: get_links_by_predicate currently returns
LinkExpression.data.predicate as Some("") when the DB stored an empty sentinel,
causing inconsistency with add_link/get_link which map "" → None; update
get_links_by_predicate to normalize the predicate value by reading the raw
string from row.get(2) and converting an empty string to None (i.e., set
Link.data.predicate = None when the retrieved predicate == ""), and consider
factoring this normalization into a shared helper used by add_link, get_link,
and get_links_by_predicate for consistency.
In `@rust-executor/src/perspectives/perspective_instance.rs`:
- Around line 1800-1820: The doc for mark_prolog_engine_dirty says it applies to
"Simple/SdnaOnly" but the implementation only checks PrologMode::Simple; either
update the runtime check to include SdnaOnly (e.g., check PROLOG_MODE ==
PrologMode::Simple || PROLOG_MODE == PrologMode::SdnaOnly) or change the doc
comment to remove SdnaOnly to match current behavior; adjust the check in the
mark_prolog_engine_dirty method (and ensure any callers like
update_prolog_engines keep expected semantics) and reference the PrologMode enum
and PROLOG_MODE global to locate where to change.
In `@rust-executor/src/prolog_service/mod.rs`:
- Around line 167-307: The map is left with unspawned placeholder engines if
load_module_string() fails after swapping engines out; wrap the expensive load
operations for query_engine_to_update and subscription_engine_to_update in
error-handling that on any Err reacquires self.simple_engines.write().await and
restores the original engines into the SimpleEngine (assign
simple_engine.query_engine = query_engine_to_update and
simple_engine.subscription_engine = subscription_engine_to_update), set
simple_engine.dirty = true (so update will be retried), then return the error;
use the existing symbols (simple_engines, SimpleEngine, query_engine_to_update,
subscription_engine_to_update, load_module_string) and avoid leaving
PrologEngine::new() placeholders in the map on failure.
♻️ Duplicate comments (5)
tests/js/tests/prolog-and-literals.test.ts (5)
495-514: Duplicate: skipped-test coverage needs tracking.
Same concern as Line 421.
516-529: Duplicate: skipped-test coverage needs tracking.
Same concern as Line 421.
785-807: Duplicate: skipped-test coverage needs tracking.
Same concern as Line 421.
2342-2395: Duplicate: skipped-test coverage needs tracking.
Same concern as Line 421.
2838-2840: Duplicate: skipped-test coverage needs tracking.
Same concern as Line 421.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
core/src/model/Ad4mModel.tscore/src/model/Subject.tsrust-executor/src/db.rsrust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rstests/js/tests/prolog-and-literals.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- core/src/model/Subject.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/perspectives/PerspectiveProxy.ts:1465-1491
Timestamp: 2026-01-15T14:55:56.365Z
Learning: The codebase is moving toward making Prolog completely optional. Do not suggest adding Prolog fallbacks or dependencies in code paths that are being migrated to SurrealDB-based implementations (SdnaOnly mode).
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: rust-executor/src/perspectives/perspective_instance.rs:2518-2532
Timestamp: 2026-01-15T17:26:10.403Z
Learning: In rust-executor/src/perspectives/perspective_instance.rs, the Simple-mode branch of spawn_prolog_facts_update() intentionally publishes diffs so both Prolog subscribers and link-only clients receive updates. Do not remove this publish or “dedupe” it by default; duplicate add/remove events are an accepted trade-off unless explicitly changed later.
📚 Learning: 2026-01-15T17:29:55.923Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/model/Ad4mModel.ts:814-822
Timestamp: 2026-01-15T17:29:55.923Z
Learning: In core/src/model/Ad4mModel.ts, when parsing literal URLs with Literal.fromUrl(value).get() in the getData() method's SurrealDB fallback path (around lines 814-822), the code correctly checks for a `data` property (`parsed.data !== undefined ? parsed.data : parsed`) to handle two cases: Expression objects (which have a data property that should be extracted when we want just the value) and other literal types (which should be used as-is). This conditional is intentional and correct *in this specific context* where we want to reduce Expressions to their data values. Note that this is not a universal pattern—Expression shapes are preserved with full metadata in other contexts where that's needed.
Applied to files:
tests/js/tests/prolog-and-literals.test.ts
📚 Learning: 2026-01-15T14:55:56.365Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/perspectives/PerspectiveProxy.ts:1465-1491
Timestamp: 2026-01-15T14:55:56.365Z
Learning: The codebase is moving toward making Prolog completely optional. Do not suggest adding Prolog fallbacks or dependencies in code paths that are being migrated to SurrealDB-based implementations (SdnaOnly mode).
Applied to files:
tests/js/tests/prolog-and-literals.test.tsrust-executor/src/perspectives/perspective_instance.rscore/src/model/Ad4mModel.tsrust-executor/src/prolog_service/mod.rs
📚 Learning: 2026-01-15T17:26:10.403Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: rust-executor/src/perspectives/perspective_instance.rs:2518-2532
Timestamp: 2026-01-15T17:26:10.403Z
Learning: In rust-executor/src/perspectives/perspective_instance.rs, the Simple-mode branch of spawn_prolog_facts_update() intentionally publishes diffs so both Prolog subscribers and link-only clients receive updates. Do not remove this publish or “dedupe” it by default; duplicate add/remove events are an accepted trade-off unless explicitly changed later.
Applied to files:
rust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rs
📚 Learning: 2025-09-03T15:16:29.623Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 619
File: rust-executor/src/perspectives/perspective_instance.rs:639-641
Timestamp: 2025-09-03T15:16:29.623Z
Learning: In the AD4M codebase, database operations like `add_many_links` during critical synchronization processes (such as `diff_from_link_language`) should use `.expect()` rather than error logging, as failures indicate severe system issues where fail-fast behavior is preferred over continuing with potentially corrupted state.
Applied to files:
rust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rs
📚 Learning: 2025-04-11T14:17:11.180Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 593
File: rust-executor/src/prolog_service/engine_pool.rs:65-89
Timestamp: 2025-04-11T14:17:11.180Z
Learning: The `run_query_all()` method in PrologEnginePool intentionally uses a write lock (not a read lock) because it can run assert/1 queries that modify facts. This ensures all engines remain synchronized and prevents other operations from executing concurrently during fact updates.
Applied to files:
rust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rs
📚 Learning: 2025-07-17T16:34:09.725Z
Learnt from: jhweir
Repo: coasys/ad4m PR: 617
File: ad4m-hooks/vue/src/useModel.ts:45-56
Timestamp: 2025-07-17T16:34:09.725Z
Learning: In ad4m-hooks/vue/src/useModel.ts, the baseExpression property is defined as a getter on the Ad4mModel prototype, not as an own property on individual instances. The current implementation correctly uses Object.defineProperty to create enumerable own properties that shadow the prototype getter, which is the appropriate approach for making prototype accessors enumerable.
Applied to files:
core/src/model/Ad4mModel.ts
📚 Learning: 2026-01-15T17:29:55.923Z
Learnt from: lucksus
Repo: coasys/ad4m PR: 644
File: core/src/model/Ad4mModel.ts:814-822
Timestamp: 2026-01-15T17:29:55.923Z
Learning: In core/src/model/Ad4mModel.ts, within the getData() method's SurrealDB fallback path (around lines 814-822), retain the conditional: when parsing literal URLs with Literal.fromUrl(value).get(), use parsed.data !== undefined ? parsed.data : parsed. This is intentional and correct for reducing Expression objects to their data values in this specific context, while preserving full metadata for other contexts where Expression shapes are needed. Do not generalize this pattern to all literal parsing; this behavior is scoped to this file and scenario.
Applied to files:
core/src/model/Ad4mModel.ts
🧬 Code graph analysis (2)
core/src/model/Ad4mModel.ts (2)
core/src/model/decorators.ts (2)
PropertyOptions(147-194)CollectionOptions(404-419)core/src/utils.ts (1)
escapeSurrealString(49-57)
rust-executor/src/prolog_service/mod.rs (4)
rust-executor/src/prolog_service/engine_pool.rs (1)
new(118-138)rust-executor/src/prolog_service/sdna_pool.rs (1)
new(231-238)rust-executor/src/prolog_service/pool_trait.rs (2)
new(74-82)preprocess_program_lines(137-147)rust-executor/src/perspectives/sdna.rs (4)
get_data_facts(656-669)get_sdna_facts(672-749)get_static_infrastructure_facts(284-653)is_sdna_link(103-111)
🔇 Additional comments (28)
tests/js/tests/prolog-and-literals.test.ts (5)
436-462: Good guard + cleanup for deterministic selection.
This avoids accidental cross-test contamination and gives a clear failure mode when prerequisites aren’t met.
538-544: Nice isolation cleanup.
Removing Message flags inafterEachshould prevent test bleed.
929-935: Order-insensitive assertion is a good stability tweak.
This avoids flaky ordering assumptions while still validating contents.
1930-1972: Polling + relaxed counts look reasonable for async subscriptions.
The looped waits andat.leastchecks make the test more resilient.
2580-2585: Polling safeguard improves subscription reliability.
The retry loop helps stabilize the async assertions.core/src/model/Ad4mModel.ts (5)
6-6: No issues in these helper additions.Also applies to: 618-634, 668-691
814-818: No issues here.
920-945: Predicate escaping hardening looks good.Also applies to: 1063-1063, 1195-1195, 1271-1273, 1309-1322
1474-1492: No issues here.
1972-1990: Metadata-driven property setter path looks consistent.rust-executor/src/db.rs (1)
3081-3102: Test looks good. Covers predicate-based filtering with two predicates and validates results independently.rust-executor/src/perspectives/perspective_instance.rs (7)
23-23: Config tweaks look fine.
ImportingPrologMode/PROLOG_MODEand tightening the subscription timeout are straightforward.Also applies to: 54-54
795-797: Consolidated Prolog update helper usage is consistent.
Good to seeupdate_prolog_engines(...)applied across the main mutation paths.Also applies to: 902-904, 1027-1030, 1207-1209, 1313-1315, 4109-4112
1338-1376: SDNA link retrieval is now properly targeted.
The two-query SDNA fetch plus indexed predicate lookup should reduce unnecessary scans.Also applies to: 1391-1392
1822-1889: Mode-aware query routing looks solid.
The Simple/SdnaOnly vs Pooled/Disabled branching is clear and consistent across the query entry points.Also applies to: 1899-1928, 1938-1959, 1970-1997, 2010-2087, 2100-2204
3679-3721: Subscription checks now avoid cloning large results.
Nice memory optimization by deferringlast_resultcloning until after change detection.Also applies to: 3757-3795
3836-3869: Periodic subscription logging is helpful for observability.
The interval-based summary should make stuck subscriptions easier to diagnose.
2371-2384: SdnaOnly still pushes non‑SDNA data into Prolog.
spawn_prolog_facts_update()only special-casesSimple. InSdnaOnly, it proceeds into assertion/full rebuild logic, which can assert all link facts and rebuild fromget_links()—undermining the SdnaOnly memory goal. Add an SdnaOnly branch to ignore non‑SDNA diffs and rebuild using SDNA-only links.Based on learnings, avoid reintroducing Prolog data facts in SdnaOnly.🛠️ Suggested direction
if PROLOG_MODE == PrologMode::Simple { log::debug!("Prolog facts update (Simple mode): marking subscription engine dirty"); *(self_clone.trigger_prolog_subscription_check.lock().await) = true; self_clone.pubsub_publish_diff(diff).await; if let Some(sender) = completion_sender { let _ = sender.send(()); } return; } + + if PROLOG_MODE == PrologMode::SdnaOnly { + let mut diff = diff; + diff.additions.retain(|l| is_sdna_link(&l.data)); + diff.removals.retain(|l| is_sdna_link(&l.data)); + if diff.additions.is_empty() && diff.removals.is_empty() { + if let Some(sender) = completion_sender { let _ = sender.send(()); } + return; + } + // Rebuild using SDNA-only links (e.g., via get_sdna_links_local) + }⛔ Skipped due to learnings
Learnt from: lucksus Repo: coasys/ad4m PR: 644 File: rust-executor/src/perspectives/perspective_instance.rs:2518-2532 Timestamp: 2026-01-15T17:26:10.403Z Learning: In rust-executor/src/perspectives/perspective_instance.rs, the Simple-mode branch of spawn_prolog_facts_update() intentionally publishes diffs so both Prolog subscribers and link-only clients receive updates. Do not remove this publish or “dedupe” it by default; duplicate add/remove events are an accepted trade-off unless explicitly changed later.Learnt from: lucksus Repo: coasys/ad4m PR: 644 File: core/src/perspectives/PerspectiveProxy.ts:1465-1491 Timestamp: 2026-01-15T14:55:56.365Z Learning: The codebase is moving toward making Prolog completely optional. Do not suggest adding Prolog fallbacks or dependencies in code paths that are being migrated to SurrealDB-based implementations (SdnaOnly mode).rust-executor/src/prolog_service/mod.rs (10)
28-52: Clear mode semantics and inline docs.PrologMode variants and the in-file guidance are clear and actionable.
56-81: SimpleEngine encapsulation + initialization look solid.
83-117: Mode guard + pool lookup helpers are straightforward.
119-137: Dirty-flag tracking is consistent with Simple/SdnaOnly behavior.
309-318: SDNA extraction helper is clean and focused.
320-406: Simple/SdnaOnly query paths look consistent and separated.
413-431: Mode short‑circuiting for pooled setup is appropriate.
488-560: Pooled-only routing + diagnostics look good.
563-700: Mode guards and pooled updates are consistent with the new lifecycle.
729-730: Test gating reflects SdnaOnly constraints.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
…with sophisticated waitForCondition()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
core/src/model/Ad4mModel.ts (1)
1272-1295: Fixsource = sourcetautology inbuildSurrealSelectFields; method appears unused.The helper uses
source = sourcein its subqueries (lines 1283, 1294, 1298–1299), which is a tautology that would cause subqueries to return all link rows rather than correlate to the parent. The JSDoc (lines 1258–1259) documents the correct syntax:source = $parent.base.Additionally, this method (and
buildSurrealSelectFieldsWithAggregation) appear to be unused dead code—no calls found in the codebase. The active query methods (queryToSurrealQL,countQueryToSurrealQL) use a different graph-traversal approach instead. Either remove these methods or correct the syntax to match the documented intent ($parent.base) if they are intended for future use.rust-executor/src/perspectives/perspective_instance.rs (1)
3837-3869: Consider lowering verbosity of periodic subscription logging.
Logging full query previews atinfoevery minute can be noisy and may expose sensitive data. Suggest keeping the count atinfoand moving per-query details todebug.Possible adjustment
- log::info!( + log::info!( "📊 Prolog subscriptions [{}]: {} active", perspective_uuid, queries.len() ); for (id, query) in queries.iter() { let query_preview = if query.query.len() > 100 { format!("{}...", &query.query[..100]) } else { query.query.clone() }; - log::info!(" - [{}]: {}", id, query_preview); + log::debug!(" - [{}]: {}", id, query_preview); }
🤖 Fix all issues with AI agents
In `@rust-executor/src/perspectives/perspective_instance.rs`:
- Around line 1338-1376: The deduplication in get_sdna_links_local collapses
distinct SDNA links that share source/predicate/target/author but differ by
timestamp (and possibly status); update the dedup key to include the link
timestamp (e.g., LinkExpression.data.timestamp or equivalent field) and
optionally the LinkStatus (or status field) when building the HashSet key used
for seen, so that links with different timestamps (or statuses) are treated as
distinct and not dropped.
🧹 Nitpick comments (4)
tests/js/tests/prolog-and-literals.test.ts (3)
421-432: Track skipped tests with a TODO/issue reference.
These skips leave coverage gaps for Prolog-only or parity paths; please link a follow‑up issue or annotate the skip reason so the gaps are intentional and discoverable.Also applies to: 495-514, 516-529, 785-807, 2369-2422, 2896-2898
1935-1955: Unify remaining polling loops withwaitForCondition.
These manual loops still rely on fixed attempts and short time windows; replacing them with the helper improves consistency and reduces flakiness.♻️ Suggested refactor (apply similarly across the remaining loops)
- for (let i = 0; i < 30; i++) { - if (updateCount >= 1 && notifications.length === 1) break; - await sleep(50); - } + await waitForCondition( + () => updateCount >= 1 && notifications.length === 1, + { + timeoutMs: 5000, + errorMessage: 'Subscription did not update after first notification save' + } + ); expect(updateCount).to.be.at.least(1); - for (let i = 0; i < 30; i++) { - if (updateCount >= 2 && notifications.length === 2) break; - await sleep(50); - } + await waitForCondition( + () => updateCount >= 2 && notifications.length === 2, + { + timeoutMs: 5000, + errorMessage: 'Subscription did not update after second notification save' + } + ); expect(updateCount).to.be.at.least(2); - for (let i = 0; i < 30; i++) { - if (notifications.length === 1) break; - await sleep(50); - } + await waitForCondition( + () => notifications.length === 1, + { + timeoutMs: 5000, + errorMessage: 'Subscription did not update after notification update' + } + ); - let attempts = 0; - while (attempts < 20 && (!pageCallback.called || pageCallback.lastCall.args[0].results.length < 2)) { - await sleep(100); - attempts++; - } + await waitForCondition( + () => pageCallback.called && pageCallback.lastCall.args[0].results.length >= 2, + { + timeoutMs: 5000, + errorMessage: 'Paginated subscription did not reach expected results' + } + );Also applies to: 1974-1978, 2625-2631
3569-3598: Comment claims exponential backoff but uses fixed interval.
Either implement backoff or adjust the docstring to match behavior.📝 Minimal fix: align comment with implementation
- * Wait for a condition to become true with exponential backoff. - * This is more reliable than fixed sleep() for async operations. + * Wait for a condition to become true with polling. + * This is more reliable than fixed sleep() for async operations.rust-executor/src/db.rs (1)
1186-1221: Consider anOptionpredicate input to avoid the empty‑string sentinel.Right now callers must pass
""to query “no predicate,” which is easy to miss. AnOption<&str>(or helper that mapsNone → "") would make the API consistent withLink.predicatesemantics.♻️ Possible direction
-pub fn get_links_by_predicate(&self, perspective_uuid: &str, predicate: &str) -> Ad4mDbResult<Vec<(LinkExpression, LinkStatus)>> { +pub fn get_links_by_predicate(&self, perspective_uuid: &str, predicate: Option<&str>) -> Ad4mDbResult<Vec<(LinkExpression, LinkStatus)>> { let mut stmt = self.conn.prepare( "SELECT perspective, source, predicate, target, author, timestamp, signature, key, status FROM link WHERE perspective = ?1 AND predicate = ?2 ORDER BY timestamp, source, target, author", )?; - let link_iter = stmt.query_map(params![perspective_uuid, predicate], |row| { + let link_iter = stmt.query_map(params![perspective_uuid, predicate.unwrap_or("")], |row| { ... })?;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
rust-executor/src/perspectives/perspective_instance.rs (1)
1085-1088: Ensure Simple/SdnaOnly query engines get marked dirty here.
add_linksandlink_mutationsstill callspawn_prolog_facts_update(...)directly, which skips the dirty mark used by Simple-mode lazy query updates. This can leaverun_query_simpleserving stale results until another path callsmark_prolog_engine_dirty(). Prefer the unified helper here as well.✅ Suggested fix
- self.spawn_prolog_facts_update(decorated_perspective_diff.clone(), None); + self.update_prolog_engines(decorated_perspective_diff.clone()).await;- self.spawn_prolog_facts_update(decorated_diff.clone(), None); + self.update_prolog_engines(decorated_diff.clone()).await;Also applies to: 1138-1141
🤖 Fix all issues with AI agents
In `@rust-executor/src/prolog_service/mod.rs`:
- Around line 247-259: engine_exists was checked under a read lock but insert()
runs later and can clobber an engine inserted concurrently; re-check under the
write lock using the HashMap Entry API to avoid the race. Replace the direct
engines.insert(...) with engines.entry(perspective_id.to_string()) and match
Entry::Vacant to insert a new SimpleEngine (constructed from query_engine,
subscription_engine, dirty, current_links, current_sdna_links) and
Entry::Occupied to leave the existing engine untouched (and ensure you properly
drop or stop the newly spawned query_engine/subscription_engine if you do not
insert them). Use the Entry key (perspective_id.to_string()) and the existing
symbols SimpleEngine, query_engine, subscription_engine, and engines to locate
and implement the change.
🧹 Nitpick comments (2)
rust-executor/src/perspectives/perspective_instance.rs (2)
3838-3873: Consider downgrading periodic subscription logs to debug.Logging query previews every ~60s at
infocan be noisy and may expose user-supplied query content. Considerdebugand/or redaction.
2374-2386: Clarify the intentional Simple-mode publish to avoid future confusion.The Simple-mode early-return both triggers subscription reruns and publishes diffs. Adding a brief comment would prevent accidental “cleanup” later.
✍️ Suggested clarification
- // In Simple mode, only update subscription engine and trigger subscription rerun + // In Simple mode, update subscription engine and publish diffs immediately. + // This is intentional to keep both Prolog subscriptions and link-only clients in sync.Based on learnings, this publish is intentional and should be documented rather than removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
rust-executor/src/prolog_service/mod.rs (1)
586-603: Guardsubscription_endedfor non-pooled modes.This method currently calls
get_pool()unconditionally; in Simple/SdnaOnly/Disabled modes it will error. Consider matching the no-op pattern used elsewhere so unsubscribe flows don’t surface spurious errors.🛠️ Suggested fix
pub async fn subscription_ended( &self, perspective_id: String, query: String, ) -> Result<(), Error> { - let pool = self.get_pool(&perspective_id).await?; + match PROLOG_MODE { + PrologMode::Disabled => { + log::trace!( + "⚠️ subscription_ended called but Prolog is DISABLED (perspective: {})", + perspective_id + ); + return Ok(()); + } + PrologMode::Simple | PrologMode::SdnaOnly => { + log::trace!( + "⚠️ subscription_ended called in Simple/SdnaOnly mode (perspective: {}) - ignoring", + perspective_id + ); + return Ok(()); + } + PrologMode::Pooled => {} + } + + let pool = self.get_pool(&perspective_id).await?;
Memory Optimization: SDNA-Only Prolog + Whisper Sharing
Problem
AD4M was experiencing excessive memory usage (up to 84GB with 2 users), causing system slowdowns on macOS and crashes on Linux. Investigation revealed multiple memory accumulation issues rather than traditional memory leaks.
Root Causes Identified
🔥 CRITICAL 1: Prolog Engines with Full Link Data (Potential 20-50GB!)
rust-executor/src/prolog_service/mod.rs🔥 CRITICAL 2: AI Service - Whisper Model Per Stream (Potential 10-30GB!)
rust-executor/src/ai_service/mod.rs:11111. Massive Query Result Cloning (CRITICAL)
rust-executor/src/perspectives/perspective_instance.rs:3397&3324last_resultstrings2. Slow Filtered Pool Cleanup
rust-executor/src/prolog_service/engine_pool.rs:45-463. Long Subscription Timeouts
rust-executor/src/perspectives/perspective_instance.rs:534. Unbounded Subscription Result Storage
Solutions Implemented
🔥 Fix 1: SDNA-Only Prolog Mode (BIGGEST WIN! 20-50GB SAVED!)
Files Modified:
rust-executor/src/prolog_service/mod.rsrust-executor/src/perspectives/sdna.rscore/src/perspectives/PerspectiveProxy.tscore/src/model/Subject.tsThe Problem: Prolog engines loaded ALL perspective links as facts, consuming 20-50GB with multiple perspectives
The Solution: Prolog engines now run in SdnaOnly mode - they ONLY contain SDNA subject class definitions, NOT link data. All instance queries go through SurrealDB instead.
How It Works:
Rust Side (
rust-executor/src/prolog_service/mod.rs):TypeScript Side - Changed query strategy:
Instance Detection (
PerspectiveProxy.ts:isSubjectInstance()):@Flag({through: "ad4m://type", value: "ad4m://message"}):Instance Discovery (
PerspectiveProxy.ts:getAllSubjectInstances()):Property Access (
Subject.ts:getPropertyValue(),getCollectionValues()):where: { isInstance: Message }) applied viaisSubjectInstance()checksCollection Filters (
PerspectiveProxy.ts:getCollectionValuesViaSurreal()):instanceFilterfrom SDNA'ssubject_class("ClassName", ...)patternsWhat Prolog IS Used For:
What Prolog is NOT Used For:
Implementation Details:
triple(Base, "predicate", "exact_target")patternsinstance/2definitionsubject_class("ClassName", OtherClass)in collection_getterImpact:
🔥 Fix 2: Arc-Based Whisper Model Sharing (SECOND BIGGEST WIN!)
Files Modified:
rust-executor/src/ai_service/mod.rsThe Problem: Every
open_transcription_stream()call loaded a NEW 500MB-1.5GB Whisper model into vRAMThe Solution: Load ONE model, share it via
Arc<Whisper>across ALL transcription streamsHow It Works:
Arc<Mutex<Option<Arc<Whisper>>>>Arc(just increments reference count - cheap!)Implementation:
Impact:
Fix 3: Aggressive Transcription Cleanup
Files Modified:
rust-executor/src/ai_service/mod.rsChanges:
Impact:
Fix 4: Remove Subscription Result Truncation
Files Modified:
rust-executor/src/perspectives/perspective_instance.rsThe Problem: Subscription results were being truncated to 100KB, which was breaking results for legitimate large queries
The Solution: Removed truncation entirely - subscription results are no longer artificially limited
Changes:
MAX_SUBSCRIPTION_RESULT_SIZEconstanttruncate_subscription_result()functionImpact: Subscriptions with large results now work correctly
Fix 5: Eliminate Excessive Cloning in Subscription Loops
Files Modified:
rust-executor/src/perspectives/perspective_instance.rsBefore:
After:
Impact: If 100 subscriptions with 1MB
last_resulteach checked every 200ms:Fix 6: Reduce Subscription Timeout
Files Modified:
rust-executor/src/perspectives/perspective_instance.rsChanges:
Impact: 5x faster cleanup of stale subscriptions
Fix #7: Aggressive Filtered Pool Cleanup
Files Modified:
rust-executor/src/prolog_service/engine_pool.rsChanges:
Impact: 7.5x faster cleanup prevents accumulation (Note: Less relevant in SdnaOnly mode since pools aren't used)
Memory Usage Breakdown - Before vs After
Before Fixes (74GB with 2 users, heavy Flux):
After Fixes (Expected ~5-10GB with 2 users):
Total Expected Savings: 42-95GB (85-95% reduction!)
Expected Results
With these fixes, memory usage should:
SDNA-Only Mode Benefits:
Key Technical Insights
Why SDNA-Only Mode Works:
Why Arc-based Whisper Sharing Works:
Arc<Whisper>doesn't duplicate weights in vRAM/RAMTesting Recommendations
SDNA/Subject Class Testing:
npm test -- prolog-and-literals- All 78 tests should passisSubjectInstance()works with flags and required propertieswhere: { isInstance: ClassName }works correctlyAI Service Testing:
"Loading shared Whisper model (ONE model for ALL streams)...""Opening transcription stream (reusing shared Whisper model)"General Monitoring:
htop(Linux)Migration Notes
Prolog Service Changes (MAJOR - BREAKING):
@Property,@Collection,@Flag, etc.) work exactly as beforeAI Service Changes:
Architecture Change: Whisper model now shared across all transcription streams
Timeout Reduction: Transcription streams timeout in 30 seconds (was 2 minutes)
Subscription Changes:
Ready for testing! Expected memory reduction: 85-95% 🚀
Key wins:
Summary by CodeRabbit
New Features
Bug Fixes & Improvements
Tests
✏️ Tip: You can customize this high-level summary in your review settings.